<?php

// Copyright (c) 2010, Meadhbh S. Hamrick
// All rights reserved.
// 
// Redistribution  and use  in source  and  binary forms,  with or  without
// modification, are  permitted provided that the  following conditions are
// met:
// 
// * Redistributions of source code must retain the above copyright notice,
//   this list of conditions and the following disclaimer.
// 
// * Redistributions  in binary  form  must reproduce  the above  copyright
//   notice, this  list of conditions  and the following disclaimer  in the
//   documentation and/or other materials provided with the distribution.
// 
// * Neither  the  name  of  the  Project  Meadhbh nor  the  names  of  its
//   contributors may be  used to endorse or promote  products derived from
//   this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY  THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
// IS" AND  ANY EXPRESS OR  IMPLIED WARRANTIES, INCLUDING, BUT  NOT LIMITED
// TO,  THE  IMPLIED  WARRANTIES  OF  MERCHANTABILITY  AND  FITNESS  FOR  A
// PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT  SHALL THE  COPYRIGHT
// HOLDER OR  CONTRIBUTORS BE LIABLE FOR ANY  DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
// TO, PROCUREMENT OF  SUBSTITUTE GOODS OR SERVICES; LOSS  OF USE, DATA, OR
// PROFITS; OR BUSINESS  INTERRUPTION) HOWEVER CAUSED AND ON  ANY THEORY OF
// LIABILITY,  WHETHER IN  CONTRACT, STRICT  LIABILITY, OR  TORT (INCLUDING
// NEGLIGENCE  OR OTHERWISE)  ARISING IN  ANY WAY  OUT OF  THE USE  OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


require_once( 'DSD.php' );

class Broker {
  const OP_UNKNOWN    = 0;
  const OP_MASK       = 63;
  const OP_INTROSPECT = 32;
  const OP_META       = 16;
  const OP_CREATE     = 8;
  const OP_READ       = 4;
  const OP_UPDATE     = 2;
  const OP_DELETE     = 1;

  const SER_MASK      = 7;
  const SER_UNKNOWN   = 0;
  const SER_XML       = 1;
  const SER_JSON      = 2;
  const SER_BINARY    = 4;

  protected $operation;
  protected $operation_mask;
  protected $request_body;
  protected $request_type;
  protected $request_type_mask;
  protected $response;
  protected $response_body;
  protected $response_type;
  protected $response_type_mask;
  protected $db_descriptor;

  protected $endpoint;
  protected $db_connection;
  protected $metadata_only;

  public static function OperationFromHTTPMethod ( $method ) {
    switch( $method ) {
    case 'OPTIONS':
      $rv = Broker::OP_INTROSPECT;
      break;

    case 'HEAD':
      $rv = Broker::OP_META;
      break;

    case 'POST':
      $rv = Broker::OP_CREATE;
      break;

    case 'GET':
      $rv = Broker::OP_READ;
      break;

    case 'PUT':
      $rv = Broker::OP_UPDATE;
      break;

    case 'DELETE':
      $rv = Broker::OP_DELETE;
      break;

    default:
      $rv = Broker::OP_UNKNOWN;
      break;
    }

    return( $rv );
  }

  public static function SerializationFromHTTPTypeHeader( $content_type ) {
    switch( $content_type ) {
    case 'application/dsd+xml':
      $rv = Broker::SER_XML;
      break;

    case 'application/dsd+json':
      $rv = Broker::SER_JSON;
      break;

    case 'application/dsd+binary':
      $rv = Broker::SER_BINARY;
      break;

    default:
      $rv = Broker::SER_UNKNOWN;
      break;
    }
    
    return( $rv );
  }

  public static function HTTPTypeFromSerialization( $ser ) {
    switch( $ser ) {
    case Broker::SER_XML:
      $rv = "application/dsd+xml";
      break;

    case Broker::SER_JSON:
      $rv = "application/dsd+json";
      break;

    case Broker::SER_BINARY:
      $rv = "application/dsd+binary";
      break;

    default:
      $rv = "text/plain";
      break;
      
    }

    return( "Content-Type: " . $rv );
  }

  /*todo: fix this*/
  public static function SerializationFromHTTPAcceptHeader( $accept ) {
    return( Broker::SER_XML );
  }

  public static function HTTPStatusLine( $status ) {
    switch( $status ) {
    case 400:
      $rv = "400 Bad Request";
      break;

    case 404:
      $rv = "404 Not Found";
      break;

    case 405:
      $rv = "405 Method Not Allowed";
      break;

    case 406:
      $rv = "406 Not Acceptable";
      break;

    case 415:
      $rv = "415 Unsupported Media Type";
      break;

    case '500':
      $rv = "500 Server Error";
      break;

    default:
      $rv = "200 OK";
      break;
    }

    return( $rv );
  }

  public static function CreateErrorResponse( $errno, $desc ) {
    $rv = <<< EOD
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="http://home.meadhbh.org/xml/xsl/brookdale_error.xsl" type="text/xsl"?>
<map>
  <key>error</key>
  <string>$errno</string>
  <key>description</key>
  <string>$desc</string>
</map>

EOD;

    return( $rv );
  }

  public function initPrefs ( $site_prefs=NULL ) {
    if( NULL == $site_prefs ) {
      $site_prefs = array();
    }

    $this->operation_mask = $site_prefs[ 'site.operations' ];

    if( NULL == $this->operation_mask ) {
      $this->operation_mask =
	( Broker::OP_INTROSPECT | Broker::OP_META | Broker::OP_CREATE |
	  Broker::OP_READ | Broker::OP_UPDATE | Broker::OP_DELETE );
    }

    $this->request_type_mask = $site_prefs[ 'site.request_types' ];

    if( NULL == $this->request_type_mask ) {
      $this->request_type_mask = 
	( Broker::SER_XML | Broker::SER_JSON | Broker::SER_BINARY );
    }

    $this->response_type_mask = $site_prefs[ 'site.response_types' ];

    if( NULL == $this->response_type_mask ) {
      $this->response_type_mask =
	( Broker::SER_XML | Broker::SER_JSON | Broker::SER_BINARY );
    }

    $this->db_descriptor = $site_prefs[ 'db.descriptor' ];
  }

  public function getOperation () {
    return( $this->operation );
  }

  public function getResponseType () {
    return( $this->response_type );
  }

  public function getResponseBody () {
    return( $this->response_body );
  }

  public function getResponseBodyLength() {
    return( strlen( $this->response_body ) );
  }

  public function setOperation( $op ) {
    $this->operation = $op & Broker::OP_MASK;
  }

  public function setRequestBody( $b ) {
    $this->request_body = $b;
  }

  public function setRequestType( $ser ) {
    $this->request_type = $ser & Broker::SER_MASK;
  }

  public function setResponseType( $ser ) {
    $this->response_type = $ser & Broker::SER_MASK;
  }

  public function setEndpoint( $endpoint ) {
    $this->endpoint = $endpoint;
  }

  public function considerOperationAndContentTypes() {
    $rv = 200;

    do {
      // is operation allowable?
      if( ( $this->operation & $this->operation_mask ) == 0 ) {
	$rv = 405;
	break;
      }

      // is the request content type acceptable?
      if( ( $this->operation & ( Broker::OP_CREATE | Broker::OP_UPDATE ) ) != 0 ) {
	if( ( $this->request_type & $this->request_type_mask ) == 0 ) {
	  $rv = 415;
	  break;
	}
      }

      // is the client asking for a content type we can provide?
      if( ( $this->operation &
	    ( Broker::OP_INTROSPECT | Broker::OP_META |
	      Broker::OP_CREATE | Broker::OP_READ ) ) != 0 ) {
	if( ( $this->response_type & $this->response_type_mask ) == 0 ) {
	  $rv = 406;
	  break;
	}
      }

    } while( 0 );

    return( $rv );
  }

  public function query() {
    $rv = 200;

    do {
      if( 200 != ( $rv = $this->openDatabase() ) ) {
	break;
      }

      if( 200 != ( $rv = $this->retrieveEndpoint() ) ) {
	break;
      }

      if( 200 != ( $rv = $this->retrieveResource() ) ) {
	break;
      }

      if( 200 != ( $rv = $this->renderResponse() ) ) {
	break;
      }

    } while( 0 );

    $this->closeDatabase();

    return( $rv );
  }

  protected function openDatabase() {
    $rv = 200;

    do {
      // did we set the db_descriptor in LocalConfig.php?
      if( NULL == $this->db_descriptor ){
	$rv = 500;
	$this->response_body =
	  Broker::CreateErrorResponse( 500, "the database descriptor was not " .
				       "set before trying to open the " .
				       "database connection" );
	break;
      }

      list( $db_type, $db_host, $db_user, $db_password, $db_database ) =
	split( ":", $this->db_descriptor );

      /* todo: add check to make sure $db_type is correct */

      $this->db_connection = mysql_connect( $db_host, $db_user, $db_password );
      
      // when the db connection is false, that means we had an error opening it.
      if( false == $this->db_connection ) {
	$rv = 500;
	/* todo: add response that includes database error */
	break;
      }

      mysql_select_db( $db_database, $this->db_connection );

    } while( 0 );

    return( $rv );
  }

  protected function retrieveEndpoint () {
    $rv = 200;

    do {
      // query the database, get info about the endpoint.
      $query = sprintf( "select id,flags,class,value,expires from endpoints where id = '%s';",
			mysql_real_escape_string( $this->endpoint) );
      $results = mysql_query( $query, $this->db_connection );

      // if the endpoint doesn't exist, flag an error
      if( ( false == $results ) || ( 1 != mysql_num_rows( $results ) ) ) {
	$this->endpoint_absent = true;
	$this->response_body = Broker::CreateErrorResponse( 404, "the resource you tried to access was not found" );
	$rv = 404;
	break;
      }

      $fields = mysql_fetch_array( $results );

      // check to see if the endpoint has expired
      $expiry = $fields[ 'expires' ];
      if( ( $expiry != NULL ) && ( ( strtotime( $expiry ) - time() ) < 0 ) )  {
	$this->expire();
	$rv = 404;
	$this->response_body = Broker::CreateErrorResponse( 404, "the resource you tried to access was not found." );
	break;
      }

      // check to see if the operation is allowed for this endpoint
      $flags = (integer) $fields['flags'];
      if( ( $this->operation & $flags ) == 0 ) {
	$rv = 405;
	break;
      }

      $this->resource_class = $fields['class'];
      $this->resource_value = $fields['value'];
    } while( 0 );
      
    return( $rv );
  }

  protected function expire () {
    /* todo: remove the expired resource and all it's children */
  }

  protected function retrieveResource() {
    $rv = 200;

    do {
      $query = "select id,name,description from resource_types where id='" .
	$this->resource_class . "';";
      $results = mysql_query( $query, $this->db_connection );

      if( ( false == $results ) || ( 1 != mysql_num_rows( $results ) ) ) {
	$this->endpoint_absent = true;
	$this->response_body = Broker::CreateErrorResponse( 500, "could not locate resource class. this is very bad." );
	$rv = 500;
	break;
      }

      $fields = mysql_fetch_array( $results );
      
      eval( "require_once('resources/" . $fields['name'] . ".php');" );
      eval( '$value = new ' . $fields['name'] . '();' );

      $value->setKey( $this->resource_value );
      $value->setConnection( $this->db_connection );

      /* todo: add content checking to see if we throw a 400 */
      if( ( Broker::OP_CREATE == $this->operation ) ||
	  ( Broker::OP_UPDATE == $this->operation ) ) {
	switch( $this->request_type ) {
	case Broker::SER_XML:
	  $value->setRequest( DSD::fromXML( $this->request_body ) );
	  break;

	case Broker::SER_JSON:
	  $value->setRequest( DSD::fromJSON( $this->request_body ) );
	  break;

	case Broker::SER_BINARY:
	  $value->setRequest( DSD::fromBinary( $this->request_body ) );
	}
      }


      /* todo: make this prettier */
      switch( $this->operation ) {
      case Broker::OP_INTROSPECT:
	$rv = $value->introspect();
	break;

      case Broker::OP_META:
	$this->metadata_only = true;
	$rv = $value->read();
	break;

      case Broker::OP_CREATE:
	$rv = $value->create();
	break;

      case Broker::OP_READ:
	$rv = $value->read();
	break;

      case Broker::OP_UPDATE:
	$rv = $value->update();
	break;

      case Broker::OP_DELETE:
	$rv = $value->delete();
	break;
      }

      $this->response = $value->getContent();
      
    } while( 0 );
    
    return( $rv );
  }
  
  protected function renderResponse() {
    $rv = 200;

    if( NULL != $this->response ) {
      switch( $this->response_type ) {
      case Broker::SER_XML:
	$this->response_body = $this->response->toXML();
	break;

      case Broker::SER_JSON:
	$this->response_body = $this->response->toJSON();
	break;

      case Broker::SER_BINARY:
	$this->response_body = $this->response->toBinary();
	break;
      }
    }

    return( $rv );
  }

  protected function closeDatabase() {
    if( false != $this->db_connection ) {
      mysql_close( $this->db_connection );
    }
  }

}

?>